iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
AI Engineering

Agent 開發, 從本地到雲端: AI 工程師的 K8s 筆記系列 第 9 篇

Day 09|requests / limits、OOMKilled、swap:把記憶體壓力事件翻譯成 K8s 語言

  • 分享至 

  • xImage
  •  

cover

Day 04 記錄了那次記憶體壓力事件的「現場」,今天把它翻譯成 K8s 的語言。目的不是說「上了 K8s 就不會再發生」,而是搞清楚:同樣的事在 K8s 裡會長什麼樣、可以在哪裡看到它,以及哪些問題就算換到 K8s 也救不了。

先談一個 K8s 原生不鼓勵的東西:swap

那次事件的核心症狀是 swap 塞滿。事後我又發現另一個更隱蔽的現象:RAM 明明還有 17–21 GiB 可用,swap 卻卡在 3.9 / 4.0 GiB。那是先前壓力尖峰留下來的殘留,kernel 不會主動把它搬回 RAM。

根因追到最後有點意外:這台機器的實體記憶體從早期的 8 GB、16 GB 一路加到 32 GB,但 vm.swappiness 一直是 Linux 預設的 60。這個預設值的假設是「記憶體很緊,要早點騰空間」;到了 32 GB 的機器上,就變成「RAM 明明還很多,系統卻提早把分頁寫進 swap」。後來我把它調成 10,並把 swapfile 從 4 GB 加到 8 GB,兩件事都不用重開機。

為什麼在 K8s 系列要提這個?因為 K8s 傳統上要求關掉 swap(kubelet 預設要求 swap off,較新版本開始支援 Linux 節點開 swap,但不是主流配置;目前狀態以官方 Nodes 頁的 swap memory 一節為準)。原因就是我踩到的那件事:swap 讓 kubelet 的記憶體帳算和 limit 執行變得不可預測,排程跟上限都失去依據。

這反過來給了我一個啟發:在沒有 swap 的世界裡,記憶體上限(有設 limit 的前提下)是硬的,超標就被殺,不會「慢慢變卡」。 看起來殘酷,但比較誠實。compose 時代那些「系統怎麼愈來愈慢」的問題,很多其實是 swap 在幫忙掩蓋。

requests 與 limits:把事件現場重排一次

用 K8s 的眼睛重看 Day 04 的幾個角色:

多個 AI coding agent session,各佔 0.4–1.6 GiB。 在 K8s 裡它們會是一個個 Pod。關鍵問題是:有沒有設 requests?requests 和 limits 都沒設的話,scheduler 會當它們幾乎不佔資源,可能全塞進同一個 node。這其實就是那台機器當時的困境:排程的那一端,完全沒有整體用量的視野。

兩個 headless Chrome renderer,42–47% CPU 跑了好幾個小時。 這是「早就該結束卻還在」的工作。翻成 K8s:它們更適合當 Job,跑完就退場;至少也該設 activeDeadlineSeconds,逾時就由系統收掉。在 compose 時代,那兩個 renderer 不是任何 compose 服務的一部分,沒有人管它們的生命週期。

ARM64 QEMU build,CPU 約 212%、持續 swap in / out。 這是 Day 06 的型態二(burst build)。翻成 K8s:用 Job 跑,並且如實填 requests(例如 CPU 2、記憶體 4 GiB),limits 也依峰值填。scheduler 發現 node 放不下時,會讓它 Pending 排隊,而不是硬塞進去拖垮同一台上的所有服務。

閒置 45 小時的 dev 容器,佔 1.25 GiB。 這是最值得玩味的一個。在 K8s 裡如果它是個 Deployment,它一樣會一直待在 node 上,K8s 不會因為「沒人用」就把 Pod 收掉。這是 K8s 也代勞不了的部分:閒置資源的回收是營運政策,不是 scheduler 的工作。差別只在於,K8s 用 kubectl top(前提是叢集有裝 metrics-server)能讓這種浪費一眼看到。

OOMKilled:K8s 怎麼決定殺誰

Day 04 提過,Linux 的 OOM killer 挑人的邏輯跟業務優先級無關。K8s 的改善方式是把 Pod 分成三個 QoS class:

QoS 條件 大致淘汰順序
Guaranteed 所有 container 的 CPU/記憶體 requests = limits 最後才被殺
Burstable 有設 requests 但小於 limits 中間
BestEffort requests 與 limits 都沒設 最先被殺

套回上面的角色:核心 bot 設 Guaranteed(用量固定,request = limit 不浪費,也確保不會先被清掉);推論引擎設 Burstable(例如平時 2.5 GiB、峰值 6 GiB);build 和 render 則可以是 BestEffort 或低優先權的 Burstable,反正可以等、可以重跑,先被殺也不傷核心服務。

超過 limit 被殺的容器會標 OOMKilled(節點層級驅逐則是 Evicted),kubectl describe pod 就直接看得到原因,比在主機上翻 dmesg 找 OOM killer 的紀錄友善很多。

三件 K8s 也救不了的事

老實說,下面這三件 K8s 幫不上忙:

  1. 閒置資源的回收:前面說了,這是政策問題。
  2. requests 的數字:K8s 只依你宣告的數字排程,數字錯了排程就錯;資源基準線還是得自己量(Day 07 講的那件事)。
  3. 沒有進叢集的外部行程:如果 node 上還跑著沒容器化的裸行程(例如 SSH 進去手動跑的程式),K8s 對它是盲的,它引發的資源競爭一樣會波及叢集裡的 Pod。

第三點往往是從 compose 走向 K8s 的過渡期最麻煩的盲區:半套的 K8s,常常比純 compose 更難除錯。

明天談 training 跟 inference 為什麼幾乎不該共用一個叢集,是第二週的收尾概念篇。

今日一句話

K8s 沒有消滅記憶體壓力,它只是把「誰在吃多少」變成看得到的數字,把「誰先被殺」變成寫得出來的規則。

延伸閱讀


上一篇
Day 08|GPU 不是「有就好」:自架 Whisper GPU backend 與 Cloud Run L4 的經驗
下一篇
Day 10|training 跟 inference 為什麼幾乎不該共用一個叢集
系列文
Agent 開發, 從本地到雲端: AI 工程師的 K8s 筆記 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言